
import sys
import threading
import wx

import ab_file
import ab_error
import ab_object

import ab_ids as ai
import ab_plugins as ap
import ab_utils as utils
import ab_viewer_panel

import ab_progress

from ab_exceptions import AofException
from ab_object_disp import ObjectDisp


class _Invoker:
    def __init__(self, app, executor, referral, options=None, showdialog=None):
        self._app = app
        self._executor = executor

        self.si = utils.si
        self.atoi = utils.atoi
        self.safestr = utils.safestr
        self.gets = utils.getString
        self.configs = self._getConfigs
        self.logger = self._getLogger()
        self.options = options

        self.objfmt = ab_object.AtrObjectFormat(app)
        self.referral = referral
        self.dialog = showdialog

    def _getConfigs(self):
        cf = self._app.readKey(ai.CfgParaConfigObject)

        # return an empty dict if config is set to ignore
        if self._app.readKey(ai.CfgParaIgnorePlxConfig):
            return dict()

        return cf.getPlxConfigsWithName(self._executor.getActiviatedPlxName())

    def _getLogger(self):
        logger = self._app.readKey(ai.CfgParaLogObject)

        return logger.getLogger()


class Executor:

    BANNED_TYPES = (ap.PlxTpOrganizer, ap.PlxTpGenerator)

    def __init__(self, parent, app):
        self.app = app
        self.ui = True
        self.error = None
        self.frame = None
        self.ret_val = False
        self.opaque = None
        self.parent = parent

        self.waitResult = None
        self.activedPlxName = None
        self.condition = threading.Condition()
        self.plxtype = ap.PlxTpUnknown
        self.plx = self.app.readKey(ai.CfgParaPlxObject)

    def run(self, afile, title='', plxs=None, data=None, start=0,
            no_ui=False, notify=None, json=False, *args, **kws):
        if json:
            fname = afile.getFilename()
            try:
                jso = ab_object.AtrObject.loadJson(fname)

                if jso.hasChildren():
                    self.ret_val = True
                    self.opaque = dict({'tree': jso})
            except ValueError:
                pass

            if not self.ret_val:
                wx.MessageBox("'%s' may not be a valid json file" % fname,
                              'Error', wx.OK | wx.CENTRE | wx.ICON_ERROR)

            return self.ret_val

        if plxs is None:
            self.plxs = kws.get('plxs', self.plx.getPlxLists((
                                ap.PlxTpConverter, ap.PlxTpFormatter)))
        elif len(plxs.keys()) == 1 and ap.PlxTpGenerator in plxs:
            self.plxs = plxs
            self.plxtype = ap.PlxTpGenerator

            opaque = self._handleLegacyGeneratorIntf(kws)
            if opaque is not None:
                for op in opaque:
                    kws[op] = opaque[op]
            else:
                return False
        else:
            self.plxs = plxs

        missed = list()
        ci = self.plx.getCategory()
        for c in ci.getCategories():
            if c not in self.BANNED_TYPES and c not in self.plxs:
                missed.append(c)

        for c, plx in self.plx.getPlxLists(missed).items():
            self.plxs[c] = plx

        self.data = data
        self.file = afile
        self.start = start

        self.ui = not no_ui
        self.options = kws.get('_options')

        if no_ui:
            if notify is None:
                def _notify(x, y):
                    return True

                notify = _notify
            self._doProcess(notify, *args, **kws)
        else:
            # shows a progress frame to prepare the data to view
            self.frame = ab_progress.ProgressFrame(self.parent, self.app,
                                                   title, self._doProcess,
                                                   True, *args, **kws)
            self.frame.CenterOnScreen(wx.BOTH)
            self.frame.SetWindowStyleFlag(wx.RESIZE_BORDER | wx.RESIZE_BOX |
                                          wx.MAXIMIZE_BOX)
            self.frame.ShowModal()

        return self.ret_val

    def getError(self):
        return self.error

    def getResult(self):
        return self.opaque

    def getTreeResult(self):
        if self.opaque:
            return self.opaque.get('tree')
        else:
            return None

    def getActiviatedPlxName(self):
        return self.activedPlxName

    def _setActiatedPlx(self, plx):
        self.activedPlxName = plx

    def _doProcess(self, notify, *args, **kws):
        if self.plxtype == ap.PlxTpGenerator:
            self.ret_val = self._doGeneratorProc(notify, *args, **kws)
        else:
            self.ret_val = self._doGenericProc(notify, *args, **kws)

    def _showPlxDialog(self, res, **args):
        self.waitResult = None
        if not self.ui:
            self.waitResult = dict()
        elif self.plxtype == ap.PlxTpGenerator and self.condition:
            self.condition.acquire()
            wx.CallAfter(self._OnUpdateUiAction,
                         self.plxs[ap.PlxTpGenerator][0], res, **args)
            self.condition.wait()
            self.condition.release()

        return self.waitResult

    def _OnUpdateUiAction(self, plx, resource, **args):
        self.condition.acquire()
        self.frame.Show(False)
        self.waitResult = self.app.getConfigure(self.parent,
                                                '"%s" Request' % plx,
                                                resource, showbutton=True)
        self.condition.notify()
        self.condition.release()
        if self.frame.IsModal():
            self.frame.ShowModal()
        else:
            self.frame.Show(True)

    # run the function in the UI thread
    def _handleLegacyGeneratorIntf(self, kws=None):
        generator = self.plxs.get(ap.PlxTpGenerator)
        if generator is not None:
            # only one organizer could be used currently
            if isinstance(generator, (list, tuple)):
                generator = generator[0]
            else:
                self.plxs[ap.PlxTpGenerator] = (generator,)

        gen_inst = self.plx.getInstance(generator)
        if gen_inst:
            self._setActiatedPlx(self.plx.getElement(generator).getShortname())

            opaque = dict()
            if self.ui and '_outf' not in (kws or list()):
                dialog = wx.FileDialog(
                    self.parent, message="Save generated file",
                    wildcard=self.plx.getExtension(generator),
                    style=wx.SAVE | wx.CHANGE_DIR)

                if dialog.ShowModal() != wx.ID_OK:
                    return None

                files = dialog.GetPaths()
                if len(files):
                    opaque['_outf'] = files[0]
            elif '_outf' not in kws:
                raise AofException('"_outf" is missed')
            else:
                opaque['_outf'] = kws['_outf']

            if utils.hasAttrs(gen_inst, ('get_wait_request',)):
                exception, ret = self.wrapper(
                    utils.getAttrFromMethods, gen_inst, ('get_wait_request',))

                if not exception and ret:
                    opaque = self.app.getConfigure(self.parent,
                                                   '"%s" Request' % generator,
                                                   ret, showbutton=True)

            return opaque

        return None

    def _doGeneratorProc(self, notify, *args, **kws):
        ret = False
        generator = self.plxs.get(ap.PlxTpGenerator)
        if generator is not None:
            # only one organizer could be used currently
            if isinstance(generator, (list, tuple)):
                generator = generator[0]

            gen_inst = self.plx.getInstance(generator)
            if gen_inst:
                self._setActiatedPlx(
                    self.plx.getElement(generator).getShortname())

                outfile = ab_file.File(self.app, kws['_outf'], 'wb',
                                       utf8=kws.get('_text', False))
                if utils.hasAttrs(gen_inst, ('execute',)):
                    exception, ret = self.wrapper(
                        utils.getAttrFromMethods, gen_inst, ('execute',),
                        (False, False), self.file, outfile, notify,
                        start=0, offset=0, data=self.data,
                        plxtype='generator', **kws)

                elif utils.hasAttrs(gen_inst, ('generate',)):
                    data = self.data
                    if self.file is not None:
                        data = self.file.read(offset=0)

                    exception, rets = self.wrapper(
                        utils.getAttrFromMethods, gen_inst, ('generate',),
                        (False, False), data)

                    ret, data = rets
                    if not exception and ret:
                        outfile.write(data=data)

        return ret

    def _doGenericProc(self, notify, *args, **kws):
        blk_size = self.app.readKey(ai.CfgParaBlockCacheSize)
        ignore_error = self.app.readKey(ai.CfgParaIgoreIntfError)
        try:
            fct = self.file
            ret_val, _, fog = self._execOrganize(notify, blk_size,
                                                 ignore_error,
                                                 fct, *args, **kws)
            if ret_val:
                fct = fog

            ret_val, cvtOpaque, fcv = self._execConvert(notify, blk_size,
                                                        ignore_error, fct,
                                                        *args, **kws)
            if not ret_val:
                fcv = fct
                cvtOpaque = None

            ret_val, fmtOpaque, fmts = self._execFormat(notify, blk_size,
                                                        ignore_error, fcv,
                                                        start=self.start,
                                                        *args, **kws)
            # NOTE: the formatter procedure could dive into the structures to
            # invoke plug-ins again and again to re-format the results from
            # other formatters.
            if ret_val:
                self._execReformat(notify, blk_size, ignore_error, fcv,
                                   fmts.getEntry(), *args, **kws)

            if self.error is None:
                ret_val = True

            self.opaque = dict({'converter': cvtOpaque,
                                'formatter': fmtOpaque,
                                'tree': fmts.getEntry()})
        except:  # pylint:disable=W0702
            ret_val = False
            if self.error is None:
                self.error = sys.exc_info()

        return ret_val

    def _execOrganize(self, notify, blk_size, ignore_error, ori, *args, **kws):
        ret_val, exception, fog = (False, False, None)

        organizer = self.plxs.get(ap.PlxTpOrganizer)
        if organizer is not None:
            # only one organizer could be used currently
            if isinstance(organizer, (list, tuple)):
                organizer = organizer[0]

        org_inst = self.plx.getInstance(organizer)
        if org_inst:
            notify(0, 'using Organizer "%s"...' % organizer)
            self._setActiatedPlx(self.plx.getElement(organizer).getShortname())
            fog = ab_file.File(self.app, None, 'wb')
            # use Organizer to generate the data
            if utils.hasAttrs(org_inst, ('execute',)):
                exception, rets = self.wrapper(
                    utils.getAttrFromMethods, org_inst, ('execute',),
                    (False, False), ori, fog, start=0, offset=0,
                    plx_type='organizer', *args, **kws)

                if exception:
                    ret_val = False
                else:
                    ret_val, _ = rets
            elif utils.hasAttrs(org_inst, ('transfer',)):
                exception, rets = self.wrapper(
                    utils.getAttrFromMethods, org_inst, ('transfer',),
                    (False, False), ori.read(),
                    plxtype='organizer', *args, **kws)

                if exception:
                    ret_val = False
                elif rets is None:
                    ret_val = False
                else:
                    ret_val = True
                    fog = ab_file.File(self.app, None, 'rb', data=rets)

        if exception and (not ignore_error):
            raise

        return ret_val, None, fog

    def _execConvert(self, notify, blk_size, ignore_error, fct, *args, **kws):
        atc, fcv = (None, None)
        ignore, exception, ret_val = (False, False, False)

        sum_plx = len(self.plxs[ap.PlxTpConverter])
        # 1. process for 'Converter'
        for k, c in enumerate(self.plxs[ap.PlxTpConverter]):
            fcv = None
            ret_val = False
            exception = False

            pc = self.plx.getInstance(c)
            elem = self.plx.getElement(c)
            notify(100 * k / sum_plx, 'using Converter "%s"...' % c)
            self.app.debug('using Converter "%s"...' % c)

            self._setActiatedPlx(elem.getShortname())
            # 1.1 reset the converter if it has 'Reset' interface
            if utils.hasAttrs(pc, ('reset',)):
                exception, _ = self.wrapper(
                    utils.getAttrFromMethods, pc, ('reset',),
                    (False, False))

            # 1.2 call 'Execute' directory if it defines the interface
            if not exception and utils.hasAttrs(pc, ('execute',)):
                fcv = ab_file.File(self.app, None, 'wb+')
                fct.seek(0)
                exception, ret_level = self.wrapper(
                    utils.getAttrFromMethods, pc, ('execute',),
                    (False, False), fct, fcv, notify,
                    start=self.start, offset=0, plxtype='converter')

                if exception or not ret_level:
                    fcv = None
                else:
                    ret_val = True
                    self.app.debug('return successfully with size: %d'
                                   % len(fcv))
            elif not exception:
                # 1.3 otherwise, use 'Validate' and 'Convert'
                # to handle the data
                ret_level = 1
                length = len(fct)

                fct.seek(0)
                while ret_level == 1:
                    exception, ret_level = self.wrapper(
                        utils.getAttrFromMethods, pc, ('validate',),
                        (False, False), fct.read(blk_size))

                    length -= blk_size
                    if length <= 0 or exception:
                        break

                if not exception and ret_level == 1:
                    exception, ret_level = self.wrapper(
                        utils.getAttrFromMethods, pc, ('validate',),
                        (False, False), '')

                if not exception and utils.hasAttrs(pc, ('reset',)):
                    exception, _ = self.wrapper(
                        utils.getAttrFromMethods, pc, ('reset',),
                        (False, False))

                if not exception and ret_level == 0:
                    attr = elem.getAttribute()
                    if utils.hasAttrs(pc, ('get_attribute',)):
                        exception, attr = self.wrapper(
                            utils.getAttrFromMethods, pc, ('get_attribute',),
                            (False, False))

                    if not exception and (attr & ap.PlxStatusReady):
                        exception, rets = self.wrapper(
                            utils.getAttrFromMethods, pc, ('convert',),
                            (False, False), '')

                        if not exception:
                            try:
                                ret_val, data = rets
                                if ret_val:
                                    fcv = ab_file.File(self.app, None,
                                                       'ab+', data=data)
                            except ValueError:
                                exception = True
                    elif not exception:
                        ret_val = True
                        length = len(fct)
                        fcv = ab_file.File(self.app, None, 'wb+')

                        fct.seek(0)
                        while ret_val:
                            exception, rets = self.wrapper(
                                utils.getAttrFromMethods, pc, ('convert',),
                                (False, False), fct.read(blk_size))

                            if not exception:
                                ret_val, data = rets
                                if ret_val:
                                    fcv.write(data)
                            length -= blk_size
                            if exception or length <= 0:
                                break

                        if not exception and ret_val:
                            exception, rets = self.wrapper(
                                utils.getAttrFromMethods, pc, ('convert',),
                                (False, False), '')

                            if not exception:
                                ret_val, data = rets
                                if ret_val:
                                    fcv.write(data)
                                    self.app.debug('return successfully '
                                                   'with size: %d' % len(fcv))

            if exception and (not ignore_error):
                ret_val = False
                break

            if ret_val:
                # read the action plug-in name
                atc = elem.getShortname()
                attr = elem.getAttribute()
                if utils.hasAttrs(pc, ('get_attribute',)):
                    exception, attr = self.wrapper(utils.getAttrFromMethods,
                                                   pc, ('get_attribute',),
                                                   (False, False))
                ignore = (attr & ap.PlxAttrNoSource) != 0
                break

        if ignore:
            return ret_val, None, fcv

        # 2. build up data
        spn = list()
        if ret_val and not exception:
            _, spn = ObjectDisp.detectFileFormat(
                self.app, fct, blk_size, notify)

        return ret_val, ab_viewer_panel.ViewerOpaque(atc, fct, spn), fcv

    def _execFormat(self, notify, blk_size, ignore_error, fcv,
                    start=0, *args, **kws):
        ignore, exception, ret_val = (False, False, False)
        atf, fmts = (None, ab_object.AtrObjectFormatEx(self.app, fcv))
        sum_plx = len(self.plxs[ap.PlxTpFormatter])
        for k, c in enumerate(self.plxs[ap.PlxTpFormatter]):
            ignore, ret_val, ret_level = (False, False, 1)
            fc = self.plx.getInstance(c)
            elem = self.plx.getElement(c)
            notify(100 * k / sum_plx, 'using Formatter "%s"...' % c)
            self.app.debug('using Formatter "%s"...' % c)

            fmts.setCurrentPlx(elem.getShortname())
            self._setActiatedPlx(elem.getShortname())
            # 1.1 reset the converter if it has 'Reset' interface
            if utils.hasAttrs(fc, ('reset',)):
                exception, _ = self.wrapper(
                    utils.getAttrFromMethods, fc, ('reset',),
                    (False, False))

            # 1.2 call 'Execute' directory if it defines the interface
            if not exception and utils.hasAttrs(fc, ('execute',)):
                fcv.seek(0)
                exception, ret_val = self.wrapper(
                    utils.getAttrFromMethods, fc, ('execute',),
                    (False, False), fcv, fmts, notify,
                    start=start, offset=0, _target=fmts,
                    plxtype='formatter')
            elif not exception:
                # 1.3 otherwise, use 'Validate' and 'Convert'
                # to handle the data
                length = len(fcv)

                fcv.seek(0)
                while ret_level == 1:
                    exception, ret_level = self.wrapper(
                        utils.getAttrFromMethods, fc, ('validate',),
                        (False, False), fcv.read(blk_size))

                    length -= blk_size
                    if exception or length <= 0:
                        break

                if not exception and ret_level == 1:
                    exception, ret_level = self.wrapper(
                        utils.getAttrFromMethods, fc, ('validate',),
                        (False, False), '')

                if not exception and utils.hasAttrs(fc, ('reset',)):
                    exception, _ = self.wrapper(
                        utils.getAttrFromMethods, fc, ('reset',),
                        (False, False))

                if not exception and ret_level == 0:
                    attr = elem.getAttribute()
                    if utils.hasAttrs(fc, ('get_attribute',)):
                        exception, attr = self.wrapper(
                            utils.getAttrFromMethods, fc, ('get_attribute',),
                            (False, False))

                    if not exception and (attr & ap.PlxStatusReady):
                        exception, rets = self.wrapper(
                            utils.getAttrFromMethods, fc, ('parse',),
                            (False, False), '', start)

                        if not exception:
                            ret_level, fmt = rets
                            if ret_level == 0:
                                fmts.updateLegacy(fmt)
                    elif not exception:
                        ret_level = 1
                        length = len(fcv)

                        fcv.seek(0)
                        while ret_level == 1:
                            exception, rets = self.wrapper(
                                utils.getAttrFromMethods, fc, ('parse',),
                                (False, False), fcv.read(blk_size), start)

                            if not exception and ret_level != 2:
                                ret_level, fmt = rets
                                fmts.updateLegacy(fmt)
                            length -= blk_size
                            if exception or length <= 0:
                                break

                        if not exception and ret_level == 1:
                            exception, rets = self.wrapper(
                                utils.getAttrFromMethods, fc, ('parse',),
                                (False, False), '', start)

                            if not exception:
                                ret_level, fmt = rets
                                if not exception and ret_level != 2:
                                    fmts.updateLegacy(fmt)

            fmts.resetLastPlx()
            if exception and (not ignore_error):
                ret_val = False
                break

            if ret_level == 0 or ret_val:
                ret_val = True
                atf = elem.getShortname()
                attr = elem.getAttribute()
                if utils.hasAttrs(fc, ('get_attribute',)):
                    exception, attr = self.wrapper(
                        utils.getAttrFromMethods, fc, ('get_attribute',),
                        (False, False))

                ignore = (attr & ap.PlxAttrNoSource) != 0
                entry = fmts.getEntry()
                entry.update(plxname=elem.getShortname())
                if (attr & ap.PlxAttrPicture) != 0:
                    entry.update(attribute=fmts.newAttributionObject(
                                 'picture'))
                break

        if ignore:
            return ret_val, None, fmts

        # 2. build up data
        spn = list()
        if not exception:
            _, spn = ObjectDisp.detectFileFormat(
                self.app, fcv, blk_size, notify)

        return ret_val, ab_viewer_panel.ViewerOpaque(atf, fcv, spn), fmts

    def _execReformat(self, notify, blk_size, ignore_error, fcv, objs,
                      level=0, *args, **kws):
        def __expandObject(obj):
            ret = False
            if obj.extractable() and not obj.hasChildren():
                fields = obj.getField()
                if len(fields or list()) > 0 and fields[0].getSize() > 0:
                    tmp = ab_file.File(self.app, None, 'rb', afile=fcv,
                                       fields=fields)
                    ret, _, fmts = self._execFormat(notify, blk_size,
                                                    ignore_error, tmp,
                                                    *args, **kws)
                    if ret:
                        objp = fmts.getEntry()
                        if obj.isUpdatable():
                            elem = self.plx.getElement(objp.extract('p'))
                            obj.update(subject=elem.getName())

                        objp.adjust(start=fields[0].getStart(), recursive=True)
                        obj.update(attribute=objp.getAttribution())
                        obj.appendChildren(objp.getChildren())

            return ret

        # too deep an expansion
        if level > self.app.readKey(ai.CfgParaTreeIgnoredLevel):
            return False

        ret_val = False
        # expand the children first than the node to avoid recursive
        for obj in objs.getChildren() or list():
            ret = self._execReformat(notify, blk_size, ignore_error, fcv,
                                     obj, level + 1, *args, **kws)
            ret_val = ret or ret_val

        return ret_val or __expandObject(objs)

    def _doReferral(self, intf_name, *args, **kws):
        ret = None

        error_enabled = not self.app.readKey(ai.CfgParaIgoreIntfError)
        # analyze the request string
        subnames = intf_name.split('.')
        if len(subnames) == 2:
            sname, referral = subnames[:2]
            elem = self.plx.getElement(sname)
            # check the called provider is enabled by the user and the settings
            if elem and ap.PlxTpProvider in self.plxs and \
                    elem.getName() not in self.plxs[ap.PlxTpProvider]:

                return ret

            if elem and elem.isEnabled() and elem.getInterface(referral):
                target = None
                if '_tgt' in kws or '_target' in kws:
                    target = kws.get('_tgt', kws.get('_target'))
                try:
                    if target is not None:
                        target.setCurrentPlx(sname)

                    self.app.info('Invoke %s.%s' % (sname, referral))
                    ret = elem.getInterface(referral)(*args, **kws)
                except:
                    if error_enabled:
                        raise
                finally:
                    if target is not None:
                        target.resetLastPlx()

            elif error_enabled:
                raise AofException('AoF Plx',
                                   'Interface "%s" is not registered or '
                                   'plugin is not enabled, please check '
                                   'the plugin with shortname "%s"'
                                   % (intf_name, sname))
        elif error_enabled:
            raise AofException('AoF Plx',
                               'Invalid referred interface name "%s", '
                               'it should be separated with a DOT like '
                               'SHT.func_name to indicate the plug-in '
                               'and interface.' % intf_name)

        return ret

    def wrapper(self, func, *args, **kws):
        ret_val = False
        exception = False

        if kws.get('plxtype') == 'generator':
            invoker = _Invoker(self.app, self, self._doReferral,
                               showdialog=self._showPlxDialog)
        elif kws.get('plxtype') == 'organizer':
            invoker = _Invoker(self.app, self, self._doReferral,
                               options=self.options)
        else:
            invoker = _Invoker(self.app, self, self._doReferral)

        try:
            ret_val = func(*args,
                           _i=invoker,
                           _invoker=invoker,
                           **kws)
        except TypeError:
            # FIXME: remove the temporary exception handler
            exc_info = sys.exc_info()
            # print ab_error.printExcInfo(exc_info)
            _, _, tb = ab_error._extractErrorInfo(exc_info)
            # len(tb) == 2 means the called function don't accept "_utils"
            # and other default parameters because interface doesnt' support.
            # the error doesn't come out from the deeper functions.
            if len(tb) <= 2:
                try:
                    ret_val = func(*args, **kws)
                except:  # pylint:disable=W0702
                    exception = True
            else:
                exception = True
        except:  # pylint:disable=W0702
            exception = True

        if exception:
            self.error = sys.exc_info()

        return exception, ret_val
